package cn.haocxy.jfm.file;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.NoSuchFileException;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 文件夹观察者
 * 
 * 监视文件夹状态变化, 包括新建文件, 删除文件, 修改文件 等等. 当这些事件发生时, 及时通知外部代码.
 * 
 * @author 蔡心宇
 *
 */
public final class DirWatcher {
	
	// 通知文件夹状态变化的最短时间间隔 (ms)
	private static final long TELL_INTERVAL = 500;

	
	// 执行监视的线程
	private final Executor executor = Executors
			.newSingleThreadExecutor();

	// 定时器
	private final ScheduledThreadPoolExecutor timer = new ScheduledThreadPoolExecutor(
			1);

	private final WatchService ws;

	private final Runnable onStateChanged;

	private final WatchKey watchKey;

	public DirWatcher(FilePath path, Runnable onStateChanged)
			throws NoSuchFileException {
		try {
			ws = FileSystems.getDefault().newWatchService();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

		if (onStateChanged == null) {
			throw new NullPointerException();
		}
		this.onStateChanged = onStateChanged;

		try {
			watchKey = path.toPath().register(ws,
					StandardWatchEventKinds.ENTRY_MODIFY,
					StandardWatchEventKinds.ENTRY_CREATE,
					StandardWatchEventKinds.ENTRY_DELETE);
		} catch (NoSuchFileException e) {
			throw e;
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

	}

	/**
	 * 开始监控文件夹状态
	 */
	@SuppressWarnings("unused")
	public void start() {

		executor.execute(() -> {

			long lastTellTime = -1;

			final AtomicBoolean hasEvent = new AtomicBoolean(false);

			while (true) {

				WatchKey key = null;

				try {
					key = ws.take();
				} catch (Exception e) {
					break;
				}

				// 防止以过快的速度向外报告状态变化, 否则有可能导致GUI卡死
				if (System.currentTimeMillis() - lastTellTime > TELL_INTERVAL) {
					onStateChanged.run();

					// 比间隔稍长一点的时间后再触发一次, 防止因为报告间隔导致的丢失事件
					timer.schedule(() -> {

							// 如果有未报告的事件则再报告一次
							if (hasEvent.get()) {
								onStateChanged.run();
							}

						}, 100 + TELL_INTERVAL, TimeUnit.MILLISECONDS);

					lastTellTime = System.currentTimeMillis();

					// 超过报告间隔, 已报告, 则没有未报告的事件
					hasEvent.set(false);
				} else {

					// 因间隔果断而不触发,仅记录有事件发生过
					hasEvent.set(true);
				}

				// 就算这里不需要知道具体发生了什么,也必须把这些时间取走
				watchKey.pollEvents();

				// 为下一轮监视做准备
				watchKey.reset();
			}

		});

	}

	/**
	 * 停止监控文件夹状态, 释放占用的资源
	 * 
	 * 停止后此对象即作废,无法再开始监控.
	 */
	public void close() {
		try {
			ws.close();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

}
